#region Header
/**
 * JsonMapper.cs
 *   JSON to .Net object and object to JSON conversions.
 *
 * The authors disclaim copyright to this source code. For more details, see
 * the COPYING file included with this distribution.
 **/
#endregion


using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Json
{
    internal struct PropertyMetadata
    {
        public MemberInfo Info;
        public bool       IsField;
        public Type       Type;
    }


    internal struct ArrayMetadata
    {
        private Type _elementType;

        public Type ElementType {
            get
            {
                return _elementType ?? typeof (JsonData);
            }

            set
            {
                _elementType = value;
            }
        }

        public bool IsArray { get; set; }

        public bool IsList { get; set; }
    }


    internal struct ObjectMetadata
    {
        private Type _elementType;

        public Type ElementType {
            get
            {
                return _elementType ?? typeof (JsonData);
            }

            set
            {
                _elementType = value;
            }
        }

        public bool IsDictionary { get; set; }

        public IDictionary<string, PropertyMetadata> Properties { get; set; }
    }

    internal delegate void ExporterFunc    (object obj, JsonWriter writer);
    public   delegate void ExporterFunc<T> (T obj, JsonWriter writer);

    internal delegate object ImporterFunc                (object input);
    public   delegate TValue ImporterFunc<TJson, TValue> (TJson input);

    public delegate IJsonWrapper WrapperFactory ();


    public class JsonMapper
    {
        #region Fields
        private static readonly int MaxNestingDepth;

        private static readonly IFormatProvider DatetimeFormat;

        private static readonly IDictionary<Type, ExporterFunc> BaseExportersTable;
        private static readonly IDictionary<Type, ExporterFunc> CustomExportersTable;

        private static readonly IDictionary<Type,
                IDictionary<Type, ImporterFunc>> BaseImportersTable;
        private static readonly IDictionary<Type,
                IDictionary<Type, ImporterFunc>> CustomImportersTable;

        private static readonly IDictionary<Type, ArrayMetadata> ArrayMetadata;
        private static readonly object ArrayMetadataLock = new Object ();

        private static readonly IDictionary<Type, IDictionary<Type, MethodInfo>> ConvOps;
        private static readonly object ConvOpsLock = new Object ();

        private static readonly IDictionary<Type, ObjectMetadata> ObjectMetadata;
        private static readonly object ObjectMetadataLock = new Object ();

        private static readonly IDictionary<Type, IList<PropertyMetadata>> TypeProperties;
        private static readonly object TypePropertiesLock = new Object ();

        private static readonly JsonWriter StaticWriter;
        private static readonly object StaticWriterLock = new Object ();
        #endregion


        #region Constructors
        static JsonMapper ()
        {
            MaxNestingDepth = 100;

            ArrayMetadata = new Dictionary<Type, ArrayMetadata> ();
            ConvOps = new Dictionary<Type, IDictionary<Type, MethodInfo>> ();
            ObjectMetadata = new Dictionary<Type, ObjectMetadata> ();
            TypeProperties = new Dictionary<Type,
                            IList<PropertyMetadata>> ();

            StaticWriter = new JsonWriter ();

            DatetimeFormat = DateTimeFormatInfo.InvariantInfo;

            BaseExportersTable   = new Dictionary<Type, ExporterFunc> ();
            CustomExportersTable = new Dictionary<Type, ExporterFunc> ();

            BaseImportersTable = new Dictionary<Type,
                                 IDictionary<Type, ImporterFunc>> ();
            CustomImportersTable = new Dictionary<Type,
                                   IDictionary<Type, ImporterFunc>> ();

            RegisterBaseExporters ();
            RegisterBaseImporters ();
        }
        #endregion


        #region Private Methods
        private static void AddArrayMetadata (Type type)
        {
            if (ArrayMetadata.ContainsKey (type))
                return;

            var data = new ArrayMetadata {IsArray = type.IsArray};

            if (type.GetInterfaces().Where(t => t.FullName == "System.Collections.IList") != null)
                data.IsList = true;

            foreach (PropertyInfo pInfo in from pInfo in type.GetProperties()
                                            where pInfo.Name == "Item"
                                            let parameters = pInfo.GetIndexParameters()
                                            where parameters.Length == 1
                                            where parameters[0].ParameterType == typeof (int)
                                            select pInfo)
            {
                data.ElementType = pInfo.PropertyType;
            }

            lock (ArrayMetadataLock) {
                try {
                    ArrayMetadata.Add (type, data);
                } catch (ArgumentException) {
                    return;
                }
            }
        }

        private static void AddObjectMetadata (Type type)
        {
            if (ObjectMetadata.ContainsKey (type))
                return;

            var data = new ObjectMetadata ();

            if (type.GetInterfaces().Where(t => t.FullName == "System.Collections.IDictionary").Any())
                data.IsDictionary = true;

            data.Properties = new Dictionary<string, PropertyMetadata> ();

            foreach (PropertyInfo propertyInfo in type.GetProperties ()) {
                if (propertyInfo.Name == "Item") {
                    ParameterInfo[] parameters = propertyInfo.GetIndexParameters ();

                    if (parameters.Length != 1)
                        continue;

                    if (parameters[0].ParameterType == typeof (string))
                        data.ElementType = propertyInfo.PropertyType;

                    continue;
                }

                var propertyMetadata = new PropertyMetadata {Info = propertyInfo, Type = propertyInfo.PropertyType};

                data.Properties.Add (propertyInfo.Name, propertyMetadata);
            }

            foreach (FieldInfo fieldInfo in type.GetFields ()) {
                var propertyMetadata = new PropertyMetadata
                                           {
                                               Info = fieldInfo,
                                               IsField = true,
                                               Type = fieldInfo.FieldType
                                           };

                data.Properties.Add (fieldInfo.Name, propertyMetadata);
            }

            lock (ObjectMetadataLock) {
                try {
                    ObjectMetadata.Add (type, data);
                } catch (ArgumentException) {
                    return;
                }
            }
        }

        private static void AddTypeProperties (Type type)
        {
            if (TypeProperties.ContainsKey (type))
                return;

            IList<PropertyMetadata> props = (from propertyInfo in type.GetProperties()
                                             where propertyInfo.Name != "Item"
                                             where !propertyInfo.IsDefined(typeof (JsonIgnoreAttribute), true)
                                             select new PropertyMetadata {Info = propertyInfo, IsField = false, Type = propertyInfo.PropertyType}).ToList();

            foreach (var propertyMetadata in from fieldInfo in type.GetFields()
                                             where !fieldInfo.IsDefined(typeof (JsonIgnoreAttribute), true)
                                             select new PropertyMetadata {Info = fieldInfo, IsField = true, Type = fieldInfo.FieldType})
            {
                props.Add (propertyMetadata);
            }

            lock (TypePropertiesLock) {
                try {
                    TypeProperties.Add (type, props);
                } catch (ArgumentException) {
                    return;
                }
            }
        }

        private static MethodInfo GetConvOp (Type t1, Type t2)
        {
            lock (ConvOpsLock) {
                if (! ConvOps.ContainsKey (t1))
                    ConvOps.Add (t1, new Dictionary<Type, MethodInfo> ());
            }

            if (ConvOps[t1].ContainsKey (t2))
                return ConvOps[t1][t2];

            MethodInfo op = t1.GetMethod (
                "op_Implicit", new[] { t2 });

            lock (ConvOpsLock) {
                try {
                    ConvOps[t1].Add (t2, op);
                } catch (ArgumentException) {
                    return ConvOps[t1][t2];
                }
            }

            return op;
        }

        private static object WrapperToObject(IJsonWrapper jsonWrapper, Type instanceType, ObjectStorage objectStorage)
        {
            if (jsonWrapper == null)
                return null;

            if (jsonWrapper.IsValueType)
            {
                Type jsonWrapperType = jsonWrapper.GetValueType();

                if (instanceType.IsAssignableFrom(jsonWrapperType))
                    return (jsonWrapper.GetValue());

                // If there's a custom importer that fits, use it
                if (CustomImportersTable.ContainsKey(jsonWrapperType) &&
                    CustomImportersTable[jsonWrapperType].ContainsKey(
                        instanceType))
                {

                    ImporterFunc importer =
                        CustomImportersTable[jsonWrapperType][instanceType];

                    return importer(jsonWrapper.GetValue());
                }

                // Maybe there's a base importer that works
                if (BaseImportersTable.ContainsKey(jsonWrapperType) &&
                    BaseImportersTable[jsonWrapperType].ContainsKey(
                        instanceType))
                {

                    ImporterFunc importer =
                        BaseImportersTable[jsonWrapperType][instanceType];

                    return importer(jsonWrapper.GetValue());
                }

                // Maybe it's an enum
                if (instanceType.IsEnum)
                    return Enum.ToObject(instanceType, jsonWrapper.GetValue());

                // Try using an implicit conversion operator
                MethodInfo methodInfo = GetConvOp(instanceType, jsonWrapperType);

                if (methodInfo != null)
                    return methodInfo.Invoke(null, new[] { jsonWrapper.GetValue() });

                //try to restore object from path
                if (jsonWrapperType == typeof(string))
                {
                    if (objectStorage.ContainsPath(jsonWrapper.GetString()))
                    {
                        return objectStorage.GetObject(jsonWrapper.GetString());
                    }
                }

                // No luck
                throw new JsonException(String.Format(
                        "Can't assign value '{0}' (type {1}) to type {2}",
                        jsonWrapper.GetValue(), jsonWrapperType, instanceType));
            }

            object instance = null;

            if (jsonWrapper.IsArray)
            {
                AddArrayMetadata(instanceType);
                ArrayMetadata arrayMetadata = ArrayMetadata[instanceType];

                if (!arrayMetadata.IsArray && !arrayMetadata.IsList)
                    throw new JsonException(String.Format(
                            "Type {0} can't act as an array",
                            instanceType));

                IList list;
                Type elementType;

                if (!arrayMetadata.IsArray)
                {
                    list = (IList)Activator.CreateInstance(instanceType);
                    elementType = arrayMetadata.ElementType;
                }
                else
                {
                    list = new ArrayList();
                    elementType = instanceType.GetElementType();
                }

                foreach (IJsonWrapper element in jsonWrapper.GetList())
                {
                    objectStorage.PushIndex(list.Count);

                    object item = WrapperToObject(element, elementType, objectStorage);
                    list.Add(item);

                    objectStorage.Pop();
                }

                if (arrayMetadata.IsArray)
                {
                    int n = list.Count;
                    instance = Array.CreateInstance(elementType, n);

                    for (int i = 0; i < n; i++)
                        ((Array)instance).SetValue(list[i], i);
                }
                else
                    instance = list;

            }
            else if (jsonWrapper.IsObject)
            {
                if (jsonWrapper.IsObjectTypeSpecified)
                    instanceType = Type.GetType(jsonWrapper.TypeDescriptor);

                AddObjectMetadata(instanceType);
                ObjectMetadata objectMetadata = ObjectMetadata[instanceType];

                instance = Activator.CreateInstance(instanceType);
                objectStorage.AddObject(instance);

                foreach (DictionaryEntry entry in jsonWrapper.GetDictionary())
                {
                    var property = (string)entry.Key;
                    var entryValue = (IJsonWrapper)entry.Value;

                    objectStorage.PushProperty(property);

                    if (objectMetadata.Properties.ContainsKey(property))
                    {
                        PropertyMetadata propertyMetadata =
                            objectMetadata.Properties[property];

                        object entryObject = WrapperToObject(entryValue, propertyMetadata.Type, objectStorage);

                        if (propertyMetadata.IsField)
                        {
                            ((FieldInfo)propertyMetadata.Info).SetValue(instance, entryObject);
                        }
                        else
                        {
                            var propertyInfo = (PropertyInfo)propertyMetadata.Info;

                            if (propertyInfo.CanWrite)
                            {
                                propertyInfo.SetValue(instance, entryObject, null);
                            }
                            else if (entryObject is IList)
                            {
                                var propertyArray = propertyInfo.GetValue(instance, null);

                                if (propertyArray == null)
                                    throw new JsonException("Cannot fill null list with values!");

                                if (propertyArray is IList)
                                {
                                    foreach (var item in (IList)entryObject)
                                    {
                                        ((IList)propertyArray).Add(item);
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        if (!objectMetadata.IsDictionary)
                            throw new JsonException(String.Format(
                                    "The type {0} doesn't have the " +
                                    "property '{1}'", instanceType, property));

                        ((IDictionary)instance).Add(
                            property,
                            WrapperToObject(entryValue, objectMetadata.ElementType, objectStorage));
                    }

                    objectStorage.Pop();
                }
            }

            return instance;
        }

        private static IJsonWrapper ObjectToWrapper(object obj, WrapperFactory factory, ObjectStorage objectStorage, int depth, bool specifyConstructor)
        {
            if (depth > MaxNestingDepth)
                throw new JsonException(
                    String.Format("Max allowed object depth reached while " +
                                   "trying to export from type {0}",
                                   obj.GetType()));

            if (obj == null)
            {
                return null;
            }

            if (obj is IJsonWrapper)
            {
                return (IJsonWrapper)obj;
            }

            IJsonWrapper instance = factory();

            if (obj is String)
            {
                instance.SetString((string)obj);
                return instance;
            }

            if (obj is Double)
            {
                instance.SetDouble((double)obj);
                return instance;
            }

            if (obj is Int32)
            {
                instance.SetInt((int)obj);
                return instance;
            }

            if (obj is Boolean)
            {
               instance.SetBoolean((bool)obj);
               return instance;
            }

            if (obj is Int64)
            {
                instance.SetLong((long)obj);
                return instance;
            }

            if (obj is Array)
            {
                instance.SetJsonType(JsonType.Array);

                int counter = 0;
                foreach (object elem in (Array)obj)
                {
                    Type arrayType = obj.GetType();
                    AddArrayMetadata(arrayType);
                    ArrayMetadata arrayMetadata = ArrayMetadata[arrayType];
                    specifyConstructor |= (elem.GetType() != arrayMetadata.ElementType);

                    objectStorage.PushIndex(counter++);
                    IJsonWrapper item = ObjectToWrapper(elem, factory, objectStorage, depth + 1, specifyConstructor);
                    instance.GetList().Add(item);
                    objectStorage.Pop();
                }

                return instance;
            }

            if (obj is IList)
            {
                instance.SetJsonType(JsonType.Array);
                int counter = 0;

                foreach (object elem in (IList)obj)
                {
                    Type arrayType = obj.GetType();
                    AddArrayMetadata(arrayType);
                    ArrayMetadata arrayMetadata = ArrayMetadata[arrayType];
                    specifyConstructor |= (elem.GetType() != arrayMetadata.ElementType);

                    objectStorage.PushIndex(counter++);
                    IJsonWrapper item = ObjectToWrapper(elem, factory, objectStorage, depth + 1, specifyConstructor);
                    instance.GetList().Add(item);
                    objectStorage.Pop();
                }

                return instance;
            }

            if (obj is IDictionary)
            {
                instance.SetJsonType(JsonType.Object);

                foreach (DictionaryEntry entry in (IDictionary)obj)
                {
                    if (!(entry.Key is string))
                        throw new KeyNotFoundException("Key must have System.String type");

                    objectStorage.PushProperty((string)entry.Key);
                    IJsonWrapper item = ObjectToWrapper(entry.Value, factory, objectStorage, depth + 1, specifyConstructor);
                    instance.GetDictionary().Add(entry.Key, item);
                    objectStorage.Pop();
                }

                return instance;
            }

            Type objectType = obj.GetType();

            // Last option, let's see if it's an enum
            if (obj is Enum)
            {
                Type enumType = Enum.GetUnderlyingType(objectType);
                
                if (enumType == typeof(long) || enumType == typeof(uint) || enumType == typeof(ulong))
                    instance.SetLong((long)obj);
                else
                    instance.SetInt((int)obj);

                return instance;
            }

            if (obj is DateTime)
            {
                instance.SetString(((DateTime)obj).ToString(DateTimeFormatInfo.InvariantInfo));
                return instance;
            }

            // Okay, so it looks like the input should be exported as an
            // object
            AddTypeProperties(objectType);
            IList<PropertyMetadata> typeProperties = TypeProperties[objectType];

            var storedPath = objectStorage.StroredPath(obj);

            if (!string.IsNullOrEmpty(storedPath))
            {
                instance.SetString(storedPath);
            }
            else
            {
                objectStorage.AddObject(obj);

                instance.SetJsonType(JsonType.Object);

                if (specifyConstructor)
                    instance.TypeDescriptor = objectType.AssemblyQualifiedName;

                foreach (PropertyMetadata propertyMetadata in typeProperties)
                {
                    objectStorage.PushProperty(propertyMetadata.Info.Name);

                    if (propertyMetadata.IsField)
                    {
                        object value = ((FieldInfo)propertyMetadata.Info).GetValue(obj);

                        if (value != null)
                            specifyConstructor |= (value.GetType() != propertyMetadata.Type);

                        IJsonWrapper valueWrapper = ObjectToWrapper(value, factory, objectStorage, depth + 1, specifyConstructor);
                        instance.GetDictionary().Add(propertyMetadata.Info.Name, valueWrapper);
                    }
                    else
                    {
                        var propertyInfo = (PropertyInfo)propertyMetadata.Info;

                        if (propertyInfo.CanRead)
                        {
                            object value = propertyInfo.GetValue(obj, null);

                            if (value != null)
                                specifyConstructor |= (value.GetType() != propertyMetadata.Type);

                            IJsonWrapper valueWrapper = ObjectToWrapper(value, factory, objectStorage, depth + 1, specifyConstructor);
                            instance.GetDictionary().Add(propertyMetadata.Info.Name, valueWrapper);
                        }
                    }

                    objectStorage.Pop();
                }
            }

            return instance;
        }

        private static IJsonWrapper ReadValue (WrapperFactory factory, JsonReader reader)
        {
            reader.Read ();

            if (reader.Token == JsonToken.ArrayEnd ||
                reader.Token == JsonToken.Null)
                return null;

            IJsonWrapper instance = factory ();

            if (reader.Token == JsonToken.String) {
                instance.SetString ((string) reader.Value);
                return instance;
            }

            if (reader.Token == JsonToken.Double) {
                instance.SetDouble ((double) reader.Value);
                return instance;
            }

            if (reader.Token == JsonToken.Int) {
                instance.SetInt ((int) reader.Value);
                return instance;
            }

            if (reader.Token == JsonToken.Long) {
                instance.SetLong ((long) reader.Value);
                return instance;
            }

            if (reader.Token == JsonToken.Boolean) {
                instance.SetBoolean ((bool) reader.Value);
                return instance;
            }

            if (reader.Token == JsonToken.ArrayStart) {
                instance.SetJsonType (JsonType.Array);

                while (true) {
                    IJsonWrapper item = ReadValue (factory, reader);
                    if (reader.Token == JsonToken.ArrayEnd)
                        break;

                    instance.GetList().Add (item);
                }
            }
            else if (reader.Token == JsonToken.ObjectStart) {
                instance.SetJsonType (JsonType.Object);

                while (true) {
                    reader.Read ();

                    if (reader.Token == JsonToken.ObjectEnd)
                        break;

                    if (reader.IsValueTypeDescriptor)
                    {
                        var jsonString = ReadValue(factory, reader);
                        instance.TypeDescriptor = jsonString.GetString();
                    }
                    else
                    {
                        var property = (string)reader.Value;
                        instance.GetDictionary()[property] = ReadValue(factory, reader);
                    }
                }

            }

            return instance;
        }

        private static void WriteValue(IJsonWrapper obj, JsonWriter writer)
        {
            if (obj == null)
            {
                writer.WriteNull();
                return;
            }

            if (obj.IsString)
            {
                writer.Write(obj.GetString());
                return;
            }

            if (obj.IsBoolean)
            {
                writer.Write(obj.GetBoolean());
                return;
            }

            if (obj.IsDouble)
            {
                writer.Write(obj.GetDouble());
                return;
            }

            if (obj.IsInt)
            {
                writer.Write(obj.GetInt());
                return;
            }

            if (obj.IsLong)
            {
                writer.Write(obj.GetLong());
                return;
            }

            if (obj.IsArray)
            {
                writer.WriteArrayStart();
                foreach (object elem in obj.GetList())
                    WriteValue((JsonData)elem, writer);
                writer.WriteArrayEnd();

                return;
            }

            if (obj.IsObject)
            {
                writer.WriteObjectStart();

                if (obj.IsObjectTypeSpecified)
                    writer.WriteTypeDescriptor(obj.TypeDescriptor);

                foreach (DictionaryEntry entry in obj.GetDictionary())
                {
                    writer.WritePropertyName((string)entry.Key);
                    WriteValue((IJsonWrapper)entry.Value, writer);
                }
                writer.WriteObjectEnd();

                return;
            }
        }

        private static void RegisterBaseExporters ()
        {
            BaseExportersTable[typeof (byte)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToInt32 ((byte) obj));
                };

            BaseExportersTable[typeof (char)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToString ((char) obj));
                };

            BaseExportersTable[typeof (DateTime)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToString ((DateTime) obj,
                                                    DatetimeFormat));
                };

            BaseExportersTable[typeof (decimal)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write ((decimal) obj);
                };

            BaseExportersTable[typeof (sbyte)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToInt32 ((sbyte) obj));
                };

            BaseExportersTable[typeof (short)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToInt32 ((short) obj));
                };

            BaseExportersTable[typeof (ushort)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToInt32 ((ushort) obj));
                };

            BaseExportersTable[typeof (uint)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write (Convert.ToUInt64 ((uint) obj));
                };

            BaseExportersTable[typeof (ulong)] =
                delegate (object obj, JsonWriter writer) {
                    writer.Write ((ulong) obj);
                };
        }

        private static void RegisterBaseImporters ()
        {
            ImporterFunc importer = input => Convert.ToByte((int) input);

            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (byte), importer);

            importer = delegate (object input) {
                return Convert.ToUInt64 ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (ulong), importer);

            importer = delegate (object input) {
                return Convert.ToSByte ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (sbyte), importer);

            importer = delegate (object input) {
                return Convert.ToInt16 ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (short), importer);

            importer = delegate (object input) {
                return Convert.ToUInt16 ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (ushort), importer);

            importer = delegate (object input) {
                return Convert.ToUInt32 ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (uint), importer);

            importer = delegate (object input) {
                return Convert.ToSingle ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (float), importer);

            importer = delegate (object input) {
                return Convert.ToDouble ((int) input);
            };
            RegisterImporter (BaseImportersTable, typeof (int),
                              typeof (double), importer);

            importer = delegate (object input) {
                return Convert.ToDecimal ((double) input);
            };
            RegisterImporter (BaseImportersTable, typeof (double),
                              typeof (decimal), importer);


            importer = delegate (object input) {
                return Convert.ToUInt32 ((long) input);
            };
            RegisterImporter (BaseImportersTable, typeof (long),
                              typeof (uint), importer);

            importer = delegate (object input) {
                return Convert.ToChar ((string) input);
            };
            RegisterImporter (BaseImportersTable, typeof (string),
                              typeof (char), importer);

            importer = delegate (object input) {
                return Convert.ToDateTime ((string) input, DatetimeFormat);
            };
            RegisterImporter (BaseImportersTable, typeof (string),
                              typeof (DateTime), importer);
        }

        private static void RegisterImporter (
            IDictionary<Type, IDictionary<Type, ImporterFunc>> table,
            Type jsonType, Type valueType, ImporterFunc importer)
        {
            if (! table.ContainsKey (jsonType))
                table.Add (jsonType, new Dictionary<Type, ImporterFunc> ());

            table[jsonType][valueType] = importer;
        }

        #endregion

        public static string ToJson (object obj)
        {
            lock (StaticWriterLock) {
                StaticWriter.Reset ();

                var wrapper = ObjectToWrapper(obj, () => new JsonData(), new ObjectStorage(), 0, StaticWriter.SpecifyTypeDescriptor);

                WriteValue(wrapper, StaticWriter);

                return StaticWriter.ToString ();
            }
        }

        public static void ToJson (object obj, JsonWriter writer)
        {
            var wrapper = ObjectToWrapper(obj, () => new JsonData(), new ObjectStorage(), 0, writer.SpecifyTypeDescriptor);
            WriteValue(wrapper, writer);
        }

        public static JsonData ToJsonData(JsonReader reader)
        {
            return (JsonData) ToWrapper ( () => new JsonData(), reader);
        }

        public static JsonData ToJsonData(TextReader reader)
        {
            var jsonReader = new JsonReader (reader);

            return (JsonData) ToWrapper ( () => new JsonData(), jsonReader);
        }

        public static JsonData ToJsonData(string json)
        {
            return (JsonData) ToWrapper ( () => new JsonData(), json);
        }

        public static JsonData ToJsonData(object obj)
        {
            return (JsonData) ObjectToWrapper(obj, () => new JsonData(), new ObjectStorage(), 0, false);
        }

        public static T ToObject<T>(JsonReader reader)
        {
            return (T) WrapperToObject(ToJsonData(reader), typeof (T), new ObjectStorage());
        }

        public static T ToObject<T> (TextReader reader)
        {
            return (T)WrapperToObject(ToJsonData(reader), typeof(T), new ObjectStorage());
        }

        public static T ToObject<T> (string json)
        {
            return (T)WrapperToObject(ToJsonData(json), typeof(T), new ObjectStorage());
        }

        public static IJsonWrapper ToWrapper (WrapperFactory factory,
                                              JsonReader reader)
        {
            return ReadValue (factory, reader);
        }

        public static IJsonWrapper ToWrapper (WrapperFactory factory,
                                              string json)
        {
            var reader = new JsonReader (json);

            return ReadValue (factory, reader);
        }

        public static void RegisterExporter<T> (ExporterFunc<T> exporter)
        {
            ExporterFunc exporterWrapper = (obj, writer) => exporter((T) obj, writer);

            CustomExportersTable[typeof (T)] = exporterWrapper;
        }

        public static void RegisterImporter<TJson, TValue> (
            ImporterFunc<TJson, TValue> importer)
        {
            ImporterFunc importerWrapper = input => importer((TJson) input);

            RegisterImporter (CustomImportersTable, typeof (TJson),
                              typeof (TValue), importerWrapper);
        }

        public static void UnregisterExporters ()
        {
            CustomExportersTable.Clear ();
        }

        public static void UnregisterImporters ()
        {
            CustomImportersTable.Clear ();
        }
    }
}
